/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.contacts.common.model.account;

import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.util.ArrayMap;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import com.android.contacts.common.R;
import com.android.contacts.common.model.dataitem.DataKind;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
 * Internal structure that represents constraints and styles for a specific data source, such as the
 * various data types they support, including details on how those types should be rendered and
 * edited.
 *
 * <p>In the future this may be inflated from XML defined by a data source.
 */
public abstract class AccountType {

  private static final String TAG = "AccountType";
  /** {@link Comparator} to sort by {@link DataKind#weight}. */
  private static Comparator<DataKind> sWeightComparator =
      new Comparator<DataKind>() {
        @Override
        public int compare(DataKind object1, DataKind object2) {
          return object1.weight - object2.weight;
        }
      };
  /** The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. */
  public String accountType = null;
  /** The {@link RawContacts#DATA_SET} these constraints apply to. */
  public String dataSet = null;
  /**
   * Package that resources should be loaded from. Will be null for embedded types, in which case
   * resources are stored in this package itself.
   *
   * <p>TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and {@link
   * #getViewContactNotifyServicePackageName()}.
   *
   * <p>There's the following invariants: - {@link #syncAdapterPackageName} is always set to the
   * actual sync adapter package name. - {@link #resourcePackageName} too is set to the same value,
   * unless {@link #isEmbedded()}, in which case it'll be null. There's an unfortunate exception of
   * {@link FallbackAccountType}. Even though it {@link #isEmbedded()}, but we set non-null to
   * {@link #resourcePackageName} for unit tests.
   */
  public String resourcePackageName;
  /**
   * The package name for the authenticator (for the embedded types, i.e. Google and Exchange) or
   * the sync adapter (for external type, including extensions).
   */
  public String syncAdapterPackageName;

  public int titleRes;
  public int iconRes;
  protected boolean mIsInitialized;
  /** Set of {@link DataKind} supported by this source. */
  private ArrayList<DataKind> mKinds = new ArrayList<>();
  /** Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. */
  private Map<String, DataKind> mMimeKinds = new ArrayMap<>();

  /**
   * Return a string resource loaded from the given package (or the current package if {@code
   * packageName} is null), unless {@code resId} is -1, in which case it returns {@code
   * defaultValue}.
   *
   * <p>(The behavior is undefined if the resource or package doesn't exist.)
   */
  @VisibleForTesting
  static CharSequence getResourceText(
      Context context, String packageName, int resId, String defaultValue) {
    if (resId != -1 && packageName != null) {
      final PackageManager pm = context.getPackageManager();
      return pm.getText(packageName, resId, null);
    } else if (resId != -1) {
      return context.getText(resId);
    } else {
      return defaultValue;
    }
  }

  public static Drawable getDisplayIcon(
      Context context, int titleRes, int iconRes, String syncAdapterPackageName) {
    if (titleRes != -1 && syncAdapterPackageName != null) {
      final PackageManager pm = context.getPackageManager();
      return pm.getDrawable(syncAdapterPackageName, iconRes, null);
    } else if (titleRes != -1) {
      return ContextCompat.getDrawable(context, iconRes);
    } else {
      return null;
    }
  }

  /**
   * Whether this account type was able to be fully initialized. This may be false if (for example)
   * the package name associated with the account type could not be found.
   */
  public final boolean isInitialized() {
    return mIsInitialized;
  }

  /**
   * @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType},
   *     {@link GoogleAccountType} or {@link ExternalAccountType}.
   *     <p>If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns
   *     {@code false}) it's considered critical, and the application will crash. On the other hand
   *     if it's not an embedded type, we just skip loading the type.
   */
  public boolean isEmbedded() {
    return true;
  }

  public boolean isExtension() {
    return false;
  }

  /**
   * @return True if contacts can be created and edited using this app. If false, there could still
   *     be an external editor as provided by {@link #getEditContactActivityClassName()} or {@link
   *     #getCreateContactActivityClassName()}
   */
  public abstract boolean areContactsWritable();

  /**
   * Returns an optional custom edit activity.
   *
   * <p>Only makes sense for non-embedded account types. The activity class should reside in the
   * sync adapter package as determined by {@link #syncAdapterPackageName}.
   */
  public String getEditContactActivityClassName() {
    return null;
  }

  /**
   * Returns an optional custom new contact activity.
   *
   * <p>Only makes sense for non-embedded account types. The activity class should reside in the
   * sync adapter package as determined by {@link #syncAdapterPackageName}.
   */
  public String getCreateContactActivityClassName() {
    return null;
  }

  /**
   * Returns an optional custom invite contact activity.
   *
   * <p>Only makes sense for non-embedded account types. The activity class should reside in the
   * sync adapter package as determined by {@link #syncAdapterPackageName}.
   */
  public String getInviteContactActivityClassName() {
    return null;
  }

  /**
   * Returns an optional service that can be launched whenever a contact is being looked at. This
   * allows the sync adapter to provide more up-to-date information.
   *
   * <p>The service class should reside in the sync adapter package as determined by {@link
   * #getViewContactNotifyServicePackageName()}.
   */
  public String getViewContactNotifyServiceClassName() {
    return null;
  }

  /**
   * TODO This is way too hacky should be removed.
   *
   * <p>This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} is
   * the authenticator package name but the notification service is in the sync adapter package. See
   * {@link #resourcePackageName} -- we should clean up those.
   */
  public String getViewContactNotifyServicePackageName() {
    return syncAdapterPackageName;
  }

  /** Returns an optional Activity string that can be used to view the group. */
  public String getViewGroupActivity() {
    return null;
  }

  public CharSequence getDisplayLabel(Context context) {
    // Note this resource is defined in the sync adapter package, not resourcePackageName.
    return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
  }

  /** @return resource ID for the "invite contact" action label, or -1 if not defined. */
  protected int getInviteContactActionResId() {
    return -1;
  }

  /** @return resource ID for the "view group" label, or -1 if not defined. */
  protected int getViewGroupLabelResId() {
    return -1;
  }

  /** Returns {@link AccountTypeWithDataSet} for this type. */
  public AccountTypeWithDataSet getAccountTypeAndDataSet() {
    return AccountTypeWithDataSet.get(accountType, dataSet);
  }

  /**
   * Returns a list of additional package names that should be inspected as additional external
   * account types. This allows for a primary account type to indicate other packages that may not
   * be sync adapters but which still provide contact data, perhaps under a separate data set within
   * the account.
   */
  public List<String> getExtensionPackageNames() {
    return new ArrayList<String>();
  }

  /**
   * Returns an optional custom label for the "invite contact" action, which will be shown on the
   * contact card. (If not defined, returns null.)
   */
  public CharSequence getInviteContactActionLabel(Context context) {
    // Note this resource is defined in the sync adapter package, not resourcePackageName.
    return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
  }

  /**
   * Returns a label for the "view group" action. If not defined, this falls back to our own "View
   * Updates" string
   */
  public CharSequence getViewGroupLabel(Context context) {
    // Note this resource is defined in the sync adapter package, not resourcePackageName.
    final CharSequence customTitle =
        getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);

    return customTitle == null ? context.getText(R.string.view_updates_from_group) : customTitle;
  }

  public Drawable getDisplayIcon(Context context) {
    return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName);
  }

  /** Whether or not groups created under this account type have editable membership lists. */
  public abstract boolean isGroupMembershipEditable();

  /** Return list of {@link DataKind} supported, sorted by {@link DataKind#weight}. */
  public ArrayList<DataKind> getSortedDataKinds() {
    // TODO: optimize by marking if already sorted
    Collections.sort(mKinds, sWeightComparator);
    return mKinds;
  }

  /** Find the {@link DataKind} for a specific MIME-type, if it's handled by this data source. */
  public DataKind getKindForMimetype(String mimeType) {
    return this.mMimeKinds.get(mimeType);
  }

  /** Add given {@link DataKind} to list of those provided by this source. */
  public DataKind addKind(DataKind kind) throws DefinitionException {
    if (kind.mimeType == null) {
      throw new DefinitionException("null is not a valid mime type");
    }
    if (mMimeKinds.get(kind.mimeType) != null) {
      throw new DefinitionException("mime type '" + kind.mimeType + "' is already registered");
    }

    kind.resourcePackageName = this.resourcePackageName;
    this.mKinds.add(kind);
    this.mMimeKinds.put(kind.mimeType, kind);
    return kind;
  }

  /**
   * Generic method of inflating a given {@link ContentValues} into a user-readable {@link
   * CharSequence}. For example, an inflater could combine the multiple columns of {@link
   * StructuredPostal} together using a string resource before presenting to the user.
   */
  public interface StringInflater {

    CharSequence inflateUsing(Context context, ContentValues values);
  }

  protected static class DefinitionException extends Exception {

    public DefinitionException(String message) {
      super(message);
    }

    public DefinitionException(String message, Exception inner) {
      super(message, inner);
    }
  }

  /**
   * Description of a specific "type" or "label" of a {@link DataKind} row, such as {@link
   * Phone#TYPE_WORK}. Includes constraints on total number of rows a {@link Contacts} may have of
   * this type, and details on how user-defined labels are stored.
   */
  public static class EditType {

    public int rawValue;
    public int labelRes;
    public boolean secondary;
    /**
     * The number of entries allowed for the type. -1 if not specified.
     *
     * @see DataKind#typeOverallMax
     */
    public int specificMax;

    public String customColumn;

    public EditType(int rawValue, int labelRes) {
      this.rawValue = rawValue;
      this.labelRes = labelRes;
      this.specificMax = -1;
    }

    public EditType setSecondary(boolean secondary) {
      this.secondary = secondary;
      return this;
    }

    public EditType setSpecificMax(int specificMax) {
      this.specificMax = specificMax;
      return this;
    }

    public EditType setCustomColumn(String customColumn) {
      this.customColumn = customColumn;
      return this;
    }

    @Override
    public boolean equals(Object object) {
      if (object instanceof EditType) {
        final EditType other = (EditType) object;
        return other.rawValue == rawValue;
      }
      return false;
    }

    @Override
    public int hashCode() {
      return rawValue;
    }

    @Override
    public String toString() {
      return this.getClass().getSimpleName()
          + " rawValue="
          + rawValue
          + " labelRes="
          + labelRes
          + " secondary="
          + secondary
          + " specificMax="
          + specificMax
          + " customColumn="
          + customColumn;
    }
  }

  public static class EventEditType extends EditType {

    private boolean mYearOptional;

    public EventEditType(int rawValue, int labelRes) {
      super(rawValue, labelRes);
    }

    public boolean isYearOptional() {
      return mYearOptional;
    }

    public EventEditType setYearOptional(boolean yearOptional) {
      mYearOptional = yearOptional;
      return this;
    }

    @Override
    public String toString() {
      return super.toString() + " mYearOptional=" + mYearOptional;
    }
  }

  /**
   * Description of a user-editable field on a {@link DataKind} row, such as {@link Phone#NUMBER}.
   * Includes flags to apply to an {@link EditText}, and the column where this field is stored.
   */
  public static final class EditField {

    public String column;
    public int titleRes;
    public int inputType;
    public int minLines;
    public boolean optional;
    public boolean shortForm;
    public boolean longForm;

    public EditField(String column, int titleRes) {
      this.column = column;
      this.titleRes = titleRes;
    }

    public EditField(String column, int titleRes, int inputType) {
      this(column, titleRes);
      this.inputType = inputType;
    }

    public EditField setOptional(boolean optional) {
      this.optional = optional;
      return this;
    }

    public EditField setShortForm(boolean shortForm) {
      this.shortForm = shortForm;
      return this;
    }

    public EditField setLongForm(boolean longForm) {
      this.longForm = longForm;
      return this;
    }

    public EditField setMinLines(int minLines) {
      this.minLines = minLines;
      return this;
    }

    public boolean isMultiLine() {
      return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
    }

    @Override
    public String toString() {
      return this.getClass().getSimpleName()
          + ":"
          + " column="
          + column
          + " titleRes="
          + titleRes
          + " inputType="
          + inputType
          + " minLines="
          + minLines
          + " optional="
          + optional
          + " shortForm="
          + shortForm
          + " longForm="
          + longForm;
    }
  }

  /**
   * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the current
   * locale.
   */
  public static class DisplayLabelComparator implements Comparator<AccountType> {

    private final Context mContext;
    /** {@link Comparator} for the current locale. */
    private final Collator mCollator = Collator.getInstance();

    public DisplayLabelComparator(Context context) {
      mContext = context;
    }

    private String getDisplayLabel(AccountType type) {
      CharSequence label = type.getDisplayLabel(mContext);
      return (label == null) ? "" : label.toString();
    }

    @Override
    public int compare(AccountType lhs, AccountType rhs) {
      return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs));
    }
  }
}
